前一篇講到:
?.
??
||
? :
其中 ?. 很常跟 API Response 一起出現。
例如:
data?.customer?.name
以前我看到這種寫法,會覺得:
為什麼要一直加問號?
後來才發現,真正的問題不是 name 有沒有值。
而是:
中間任何一層都有可能根本不存在。
假設 API 回:
const data = {
customer: {
name: "ABC Company",
},
};
如果要拿公司名稱:
data.customer.name
流程其實是:
data
↓
customer
↓
name
結果:
ABC Company
這時候完全沒問題。
假設 API 改成:
const data = {
customer: null,
};
如果還寫:
data.customer.name
就會出錯。
因為程式其實在做:
data
↓
customer
↓
null
↓
還想繼續拿 .name
但:
null.name
是不成立的。
所以 JavaScript 會直接丟 Error。
這和:
customer.name
最後的 name 是不是 undefined,
其實是兩件不同的事。
?. 是在中間幫忙踩煞車改成:
data.customer?.name
意思可以先理解成:
customer 如果存在,就繼續拿 name;如果 customer 是 null 或 undefined,就停下來。
所以:
data.customer?.name
在:
customer = null
時,不會噴錯。
而是得到:
undefined
流程:
data
↓
customer
↓
null
↓
?. 發現不能繼續
↓
停止
↓
undefined
data 本身都有可能不存在呢?例如 API 還沒回來之前:
data = undefined;
這時候:
data.customer?.name
一樣會出錯。
因為第一步:
data.customer
就已經不成立。
所以要寫:
data?.customer?.name
意思就是:
data 有嗎?
↓
有才繼續
customer 有嗎?
↓
有才繼續
最後拿 name
只要其中一層是:
null
undefined
就停止。
因為實際 API Response 不一定每一筆資料結構都完整。
例如:
const order = {
customer: {
company: {
name: "ABC",
},
},
};
可能另一筆資料是:
const order = {
customer: {
company: null,
},
};
甚至:
const order = {
customer: null,
};
所以:
order.customer.company.name
其實是在假設:
customer 一定存在
company 一定存在
name 一定存在
只要其中一個假設不成立,程式就會壞掉。
所以常看到:
order?.customer?.company?.name
可以翻成人話:
order 有嗎?
↓
customer 有嗎?
↓
company 有嗎?
↓
name
如果其中一層不存在:
停止
↓
回傳 undefined
而不是直接噴錯。
?. 不代表資料一定正確這也是很重要的一點。
例如:
order?.customer?.company?.name
如果最後得到:
undefined
只能代表:
某一層沒有值。
但不能直接知道是哪一層。
可能是:
order 不存在
也可能是:
customer 不存在
也可能是:
company 不存在
也可能只是:
name 沒有值
所以如果真的在 Debug,
不能只看到:
undefined
就結束。
還是要逐層確認。
例如畫面:
order?.customer?.company?.name ?? "N/A"
結果顯示:
N/A
以前可能直接想:
公司名稱沒有資料。
但其實還不知道是哪裡斷掉。
我會開始看:
console.log(order);
console.log(order?.customer);
console.log(order?.customer?.company);
console.log(order?.customer?.company?.name);
逐層確認:
order 有嗎?
↓
customer 有嗎?
↓
company 有嗎?
↓
name 有嗎?
這樣才能真的知道問題在哪。
?. 和 ?? 很常一起用例如:
order?.customer?.company?.name ?? "N/A"
這兩個符號其實各自負責不同工作。
?.
負責:
安全地往巢狀資料裡面拿。
而:
??
負責:
如果最後得到 null / undefined,就使用預設值。
所以整句可以翻成:
先安全地找:
order
↓
customer
↓
company
↓
name
如果最後沒有值
↓
顯示 N/A
?. 不會幫你處理所有 falsy 值例如:
const data = {
amount: 0,
};
寫:
data?.amount
結果仍然是:
0
因為 ?. 只在前面的值是:
null
undefined
時停止。
它不會把:
0
""
false
當成不存在。
這點跟上一篇的 || 很不一樣。
例如:
const user = {
name: "小潔",
company: {
name: "ABC",
address: {
city: "Taipei",
},
},
};
可以畫成:
user
│
├─ name
│
└─ company
│
├─ name
│
└─ address
│
└─ city
所以:
user.company.address.city
就是一路往裡面找:
user
↓
company
↓
address
↓
city
這就是所謂的:
Nested Data
巢狀資料。
例如:
{
"id": 123,
"customer": {
"id": 10,
"name": "ABC Company"
}
}
前端拿:
data.customer.name
這很常見。
但也可能 API 回的是:
{
"id": 123,
"customer": null
}
這時候就需要考慮:
data.customer?.name
而不是直接假設:
customer 永遠存在
實際專案裡很常遇到:
API 回來的資料不是:
{
label: "ABC",
value: 1,
}
而是:
{
id: 1,
customer: {
name: "ABC",
},
}
但 Select 可能需要:
{
label: "ABC",
value: 1,
}
那前端就會需要轉換:
const options = data.map(item => ({
label: item.customer?.name ?? "N/A",
value: item.id,
}));
流程就是:
API 原始資料
↓
取巢狀欄位
↓
整理成 UI Component 需要的格式
↓
給 Select 使用
所以很多時候前端不是單純:
API 回什麼,我就直接顯示什麼。
還需要把資料重新整理成 UI 需要的形狀。
?.,也不要直接全部照抄例如:
data?.customer?.company?.address?.city
雖然很安全,
但也要想一下:
這些欄位真的都可能不存在嗎?
如果按照 TypeScript 型別或 API Contract:
data 一定存在
customer 一定存在
company 才是 optional
那可能只需要:
data.customer.company?.name
如果什麼地方都亂加:
?.
雖然不一定出錯,
但也可能把原本應該暴露出來的資料問題悄悄吞掉。
所以 ?. 的目的不是:
防越多越好。
而是:
在「真的可能是 null / undefined」的地方安全處理。
例如看到:
data.customer.company.name
我會先拆成:
data
↓
customer
↓
company
↓
name
然後問:
哪一層可能是 null?
哪一層可能是 undefined?
API Contract 怎麼定義?
再決定:
?.
要放在哪裡。
這比看到 Error 之後直接:
data?.customer?.company?.name
全部加滿,更容易真的理解資料。
Nested Data
→ 資料裡面還包著其他資料
而:
?.
是在:
中間某一層可能是
null / undefined時,安全停止取值。
例如:
data?.customer?.name
不是在問:
name 有沒有內容?
而是在確保:
拿 name 之前
↓
data 跟 customer 至少可以繼續往下取
最後再搭配:
??
像:
data?.customer?.name ?? "N/A"
就是:
安全取得 name
↓
真的沒有值
↓
顯示 N/A
下一篇可以繼續接巢狀資料另一個很常見的實務問題:
API 回來的資料形狀跟 UI 要的不一樣時,為什麼前端常常要 map() 重新整理資料?